<template>
  <div
    class="dynamic-virtual-list"
    ref="scrollBox"
    @scroll.passive="handleScroll"
  >
    <!-- 占位层 -->
    <div :style="{ height: totalHeight + 'px' }"></div>
    <!-- 可视区域 -->
    <div
      class="visible-items"
      :style="{ transform: `translateY(${offsetHeight}px)` }"
      will-change="transform"
    >
      <div
        v-for="item in visibleData"
        :key="'visibleData-' + item.index"
        ref="itemRefs"
        class="list-item"
      >
        <slot :item="item"></slot>
      </div>
    </div>
  </div>
</template>

<script setup name='JDynamicVirtualList'>
import {
  reactive,
  computed,
  watchEffect,
  onMounted,
  onUnmounted,
  nextTick,getCurrentInstance ,ref
} from "vue";
const context = getCurrentInstance();
const itemRefs=ref(null)
const props = defineProps({
  data: { type: Array, required: true }, // 原始数据
  estimatedHeight: { type: Number, default: 50 }, // 预估高度
  buffer: { type: Number, default: 3 }, // 缓冲项数
  visibleCount: { type: Number, default: 5 },
});
const state = reactive({
  scrollTop: 0,
  startIndex: 0,
  positions: [],
  lastScrollTime: 0,
  scrollTimer: null,
  offsetHeight: 0,
});
// 总高度计算
const totalHeight = computed(() => {
  if (!state.positions.length) return 0;
  const last = state.positions[state.positions.length - 1];
  return last.top + last.height;
});
const dataCopy = computed(() => {
  return props.data.map((d, i) => ({ ...d, index: i }));
});
const visibleData = computed(() => {
  const endIndex = state.startIndex + props.visibleCount + props.buffer;
  if(dataCopy.slice)
  return dataCopy.slice(
    Math.max(0, state.startIndex - props.buffer),
    Math.min(endIndex, props.data.length)
  );
  else return []
});
watchEffect(() => {
  if (props.data) {
    initPositions();
    nextTick(updateItemSizes);
  }
});
watchEffect(() => {
  if (state.startIndex) {
    updateOffsetHeight();
  }
});
onMounted(() => {
  initPositions();
  nextTick(() => {
    updateItemSizes();
    window.addEventListener("resize", handleResize);
  });
});
onUnmounted(() => {
  window.removeEventListener("resize", handleResize);
});
// 初始化位置数组
function initPositions() {
  state.positions = props.data.map((d, i) => ({
    top: i * props.estimatedHeight,
    height: props.estimatedHeight,
  }));
}

// 更新真实高度
function updateItemSizes() {

    if(!context.refs.itemRefs) return
  context.refs.itemRefs.forEach((el, ind) => {
    const index = visibleData[ind].index;
    const realHeight = el.getBoundingClientRect().height;
    if (Math.abs(realHeight - state.positions[index].height) > 2) {
      state.positions[index].height = realHeight;
      // 更新后续项top值
      for (let i = index + 1; i < state.positions.length; i++) {
        state.positions[i].top =
          state.positions[i - 1].top + state.positions[i - 1].height;
      }
    }
  });
}

function handleScroll() {
  const now = Date.now();
  if (now - state.lastScrollTime < 20) {
    if (state.scrollTimer) clearTimeout(state.scrollTimer);
    state.scrollTimer = setTimeout(() => {
      _doScrollUpdate();
    }, 30);
    return;
  }
  _doScrollUpdate();
  state.lastScrollTime = now;
}

function updateOffsetHeight() {
  if (visibleData[0].index === 0) return 0;
  const scrollTop = state.scrollTop;
  let index = state.startIndex;
  const diff = scrollTop - state.positions[index].top;
  let height = 0;
  for (const item of visibleData) {
    if (item.index >= index) break;
    height += state.positions[item.index].height;
  }
  state.offsetHeight = scrollTop - height - diff;
}

// 滚动事件处理
function _doScrollUpdate() {
  requestAnimationFrame(() => {
    state.scrollTop = context.refs.scrollBox.scrollTop;
    state.startIndex = findNearestIndex(state.scrollTop);
    nextTick(updateItemSizes);
  });
}

// 二分查找定位
function findNearestIndex(scrollTop) {
  let low = 0,
    high = state.positions.length - 1;
  while (low <= high) {
    const mid = Math.floor((low + high) / 2);
    const midVal = state.positions[mid].top;
    if (midVal === scrollTop) return mid;
    else if (midVal < scrollTop) low = mid + 1;
    else high = mid - 1;
  }
  return high < 0 ? 0 : high;
}

// 窗口尺寸变化处理
function handleResize() {
  nextTick(() => {
    updateItemSizes();
  });
}
</script>

<style scoped lang="scss">
.dynamic-virtual-list {
  height: 100%;
  overflow-y: auto;
  position: relative;
}

.visible-items {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
}
</style>